package com.lipaluma.csv.parser;

import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Collection;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.lipaluma.context.Context;
import com.lipaluma.context.mapping.impl.BuilderMappingManager;
import com.lipaluma.context.mapping.impl.ColumnMappingManager;
import com.lipaluma.csv.annotations.config.CSV;
import com.lipaluma.csv.config.CsvConfiguration;
import com.lipaluma.csv.io.CsvSourceFactory;
import com.lipaluma.csv.xml.XmlCsvMappings;
import com.lipaluma.csv.xml.configuration.XmlCsvConfiguration;
import com.lipaluma.exceptions.CsvParserException;
import com.lipaluma.exceptions.CsvParserProcessException;
import com.lipaluma.field.mapper.FieldMapper;
import com.lipaluma.field.mapper.mapping.Column;
import com.lipaluma.field.mapper.mapping.MappingInfo;
import com.lipaluma.field.validator.strategies.ValidationStrategyEnum;
import com.lipaluma.io.Row;
import com.lipaluma.io.SourceReader;
import com.lipaluma.io.SourceWriter;
import com.lipaluma.parser.AbstractAllDataParser;
import com.lipaluma.util.FileUtils;
import com.lipaluma.xml.XmlMappings;
import com.lipaluma.xml.configuration.XmlConfiguration;

public class CsvParser<TARGET> extends AbstractAllDataParser<TARGET, CsvConfiguration> {
	private final Logger LOG = LoggerFactory.getLogger(CsvParser.class);
	
	/**
	 * Constructor with the target class defined where the mapping will be defined by annotations
	 * @param clazz target class
	 */
	private CsvParser(Class<TARGET> clazz) {
		super(clazz);
	}
	
	/**
	 * Constructor with the target class defined where the mapping will be defined by xml
	 * @param clazz target class
	 * @param xml mapping xml
	 */
	private CsvParser(Class<TARGET> clazz, XmlMappings xmlMappings) {
		super(clazz, xmlMappings);
	}
	
	/**
	 * constructor used 
	 * @param builder
	 */
	private CsvParser(CsvParser.Builder<TARGET> builder) {
		super(builder);
	}
	
	public static <TARGET> CsvParser<TARGET> forClass(Class<TARGET> clazz) {
		CsvParser<TARGET> parser = new CsvParser<TARGET>(clazz);
		return parser;
	}
	
	public static <TARGET> CsvParser<TARGET> forClass(Class<TARGET> clazz, ValidationStrategyEnum validationStrategy) {
		CsvParser<TARGET> parser = forClass(clazz);
		parser.setValidationStrategy(validationStrategy);
		return parser;
	}

	public static <TARGET> CsvParser<TARGET> byXml(Class<TARGET> clazz, InputStream xml) {
		try {
			JAXBContext context = JAXBContext.newInstance(XmlCsvMappings.class);
			Unmarshaller unmarshaller = context.createUnmarshaller();
			XmlMappings xmlMappings = (XmlMappings)unmarshaller.unmarshal(xml);
			return new CsvParser<TARGET>(clazz, xmlMappings);
		} catch (JAXBException e) {
			throw new CsvParserException("Error on parsing the xml file of mappings", e);
		}
	}

	/**
	 * Construct a Mapper for the entity target class
	 * 
	 * @param file path of the xml file configuration
	 * @param clazz target class
	 * @return row mapper
	 */
	public static <TARGET> CsvParser<TARGET> byXml(Class<TARGET> clazz, String xmlFile) throws FileNotFoundException {
		return byXml(clazz, FileUtils.getInputStreamFromFilePath(xmlFile));
	}
	
	/**
	 * Processes all rows of the entry file (defined by the filepath) and returns a list of objects mapped in the target entity
	 * <p>
	 * The type of the file is determined by the extension of the filename to recover the FileType defined in the POJO.<br/>
	 * 
	 * If the file is in the classpath, put the keyword 'classpath:' before the filename path.
	 * 
	 * @param filepath the path to the file. To search in the classpath, put 'classpath:' before.
	 * @param header true if entry file has header
	 * @return List of objects mapped
	 * @throws FileNotFoundException when the file is not found
	 */
	public List<TARGET> unmarshal(String filepath) throws FileNotFoundException {
		InputStream inputStream = FileUtils.getInputStreamFromFilePath(filepath);
		SourceReader fileReader = sourceFactory.createSourceReader(inputStream);
		return unmarshalAll(fileReader);
	}

	/**
	 * Processes all rows of the entry file (defined by the reader) and returns a list of objects mapped in the target entity
	 * <p>
	 * The type of the file is defined by the configuration defined on the POJO. <br/>
	 * If several types are defined, the order of the default configuration is
	 * CSV, Excel, XLSX.
	 * 
	 * @param reader the entry file
	 * @param header if true, the process will skip one line and will try to map column names
	 * @return list of Object mapped to the target entity
	 */
	public List<TARGET> unmarshal(Reader reader) {
		SourceReader fileReader = sourceFactory.createSourceReader(reader);
		return unmarshalAll(fileReader);
	}
	
	/**
	 * Processes all rows of the entry file (defined by the InputStream) and returns a list of objects mapped in the target entity
	 * <p>
	 * The type of the file is defined by the configuration defined on the POJO. <br/>
	 * If several types are defined, the order of the default configuration is
	 * CSV, Excel, XLSX.
	 * 
	 * @param in the entry file
	 * @param header if true, the process will skip one line and will try to map column names
	 * @return list of Object mapped to the target entity
	 */
	public List<TARGET> unmarshal(InputStream in) {
		SourceReader fileReader = sourceFactory.createSourceReader(in);
		return unmarshalAll(fileReader);
	}

	@Override
	protected List<TARGET> unmarshalAll(SourceReader fileReader) {
		processHeader(fileReader);
		return super.unmarshalAll(fileReader);
	}
	
	public void processAllObjects(Collection<TARGET> objects, OutputStream out) {
		SourceWriter fw = sourceFactory.createSourceWriter(out);
		marshalAll(objects, fw);
	}
	
	public void marshalAll(Collection<TARGET> objects, Writer writer) {
		SourceWriter fw = sourceFactory.createSourceWriter(writer);
		marshalAll(objects, fw);
	}

	@Override
	protected void marshalAll(Collection<TARGET> objects, SourceWriter fw) {
		processHeader(fw);
		super.marshalAll(objects, fw);
	}
	
	@Override
	protected CsvConfiguration createConfiguration(Class<TARGET> clazz) {
		if(clazz.isAnnotationPresent(CSV.class))
			return new CsvConfiguration(clazz.getAnnotation(CSV.class));
		else
			return new CsvConfiguration();
	}
	
	@Override
	protected CsvConfiguration createConfiguration(Class<TARGET> clazz, XmlConfiguration configuration) {
		if(!XmlCsvConfiguration.class.equals(configuration.getClass()))
			throw new CsvParserException("Error on setting the configuration in the mapping xml. The configuration must be for csv");
		return new CsvConfiguration((XmlCsvConfiguration)configuration);
	}
	
	protected void processHeader(SourceReader fileReader) {
		if(configuration.hasHeader()) {
			for(int i=1; i<=configuration.getLinesHeaderNumber(); i++) {
				Row row = fileReader.next();
				if(configuration.getHeaderLine() == i && configuration.isColumnsMappedByHeaders()){
					mapHeaders(row.toArray());
				}
			}
		} else if(configuration.isColumnsMappedByHeaders())
			throw new CsvParserProcessException("Error in CSV configuration : mapping by header's name is mentioned without header");
	}
	
	public void mapHeaders(String[] headers) {
		for(FieldMapper mapper : fieldMappers) {
			for (MappingInfo mapping : mapper.getMappings()) {
				if(Column.class.isAssignableFrom(mapping.getClass())){
					for (int index = 0; index < headers.length; index++) {
						if(((Column)mapping).mapHeader(headers[index], index))
							break;
					}
				}
			}
		}
	}

	protected void processHeader(SourceWriter fileWriter) {
		if(configuration.writeHeader()) {
			Row row = fileWriter.createNewRow();
			for(FieldMapper mapper : fieldMappers) {
				MappingInfo[] mappings = mapper.getMappings();
				for (int i = 0; i < mappings.length; i++) {
					Column column = (Column)mappings[i];
					column.writeHeader(row);
				}
			}
		}
	}
	
	public void writeHeaders(String[] headers) {
		for (int index = 0; index < headers.length; index++) {
			for(FieldMapper fieldMapper : fieldMappers) {
				Column[] columns = (Column[])fieldMapper.getMappings();
				for (Column column : columns) {
					if(column.getColumnIndex() == index) {
						column.setHeaderName(headers[index]);
					}
				}
			}			
		}
	}
	
	public void setCsvConfiguration(CsvConfiguration configuration) {
		this.configuration = configuration;
		this.sourceFactory = new CsvSourceFactory(configuration.getDelimiter(), configuration.getQuote(), configuration.isAllValuesTrimed());
	}
	
	@Override
	protected Context createContext() {
		return new Context(new ColumnMappingManager());
	}
	
	public static <TARGET> Builder<TARGET> builder(Class<TARGET> targetClass) {
		return new Builder<TARGET>(targetClass);
	}

	public static class Builder<TARGET> extends AbstractAllDataParser.Builder<CsvParser<TARGET>, TARGET, CsvConfiguration> {
		public Builder(Class<TARGET> clazz) {
			super(clazz, new Context(new BuilderMappingManager()));
		}
		
		public CsvParser<TARGET> build() {
			if(configuration == null)
				configuration = new CsvConfiguration();
			return new CsvParser<TARGET>(this);
		}
	}
}
